(function() {
    function Emitter(obj) {
        if (obj) return mixin(obj);
    }
    /**
     * Mixin the emitter properties.
     *
     * @param {Object} obj
     * @return {Object}
     * @api private
     */

    function mixin(obj) {
        for (var key in Emitter.prototype) {
            obj[key] = Emitter.prototype[key];
        }
        return obj;
    }

    /**
     * Listen on the given `event` with `fn`.
     *
     * @param {String} event
     * @param {Function} fn
     * @return {Emitter}
     * @api public
     */

    Emitter.prototype.on =
        Emitter.prototype.addEventListener = function(event, fn) {
            this._callbacks = this._callbacks || {};
            (this._callbacks[event] = this._callbacks[event] || [])
            .push(fn);
            return this;
        };

    /**
     * Adds an `event` listener that will be invoked a single
     * time then automatically removed.
     *
     * @param {String} event
     * @param {Function} fn
     * @return {Emitter}
     * @api public
     */

    Emitter.prototype.once = function(event, fn) {
        var self = this;
        this._callbacks = this._callbacks || {};

        function on() {
            self.off(event, on);
            fn.apply(this, arguments);
        }

        on.fn = fn;
        this.on(event, on);
        return this;
    };

    /**
     * Remove the given callback for `event` or all
     * registered callbacks.
     *
     * @param {String} event
     * @param {Function} fn
     * @return {Emitter}
     * @api public
     */

    Emitter.prototype.off =
        Emitter.prototype.removeListener =
        Emitter.prototype.removeAllListeners =
        Emitter.prototype.removeEventListener = function(event, fn) {
            this._callbacks = this._callbacks || {};

            // all
            if (0 == arguments.length) {
                this._callbacks = {};
                return this;
            }

            // specific event
            var callbacks = this._callbacks[event];
            if (!callbacks) return this;

            // remove all handlers
            if (1 == arguments.length) {
                delete this._callbacks[event];
                return this;
            }

            // remove specific handler
            var cb;
            for (var i = 0; i < callbacks.length; i++) {
                cb = callbacks[i];
                if (cb === fn || cb.fn === fn) {
                    callbacks.splice(i, 1);
                    break;
                }
            }
            return this;
        };

    /**
     * Emit `event` with the given args.
     *
     * @param {String} event
     * @param {Mixed} ...
     * @return {Emitter}
     */

    Emitter.prototype.emit = function(event) {
        this._callbacks = this._callbacks || {};
        var args = [].slice.call(arguments, 1),
            callbacks = this._callbacks[event];

        if (callbacks) {
            callbacks = callbacks.slice(0);
            for (var i = 0, len = callbacks.length; i < len; ++i) {
                callbacks[i].apply(this, args);
            }
        }

        return this;
    };

    /**
     * Return array of callbacks for `event`.
     *
     * @param {String} event
     * @return {Array}
     * @api public
     */

    Emitter.prototype.listeners = function(event) {
        this._callbacks = this._callbacks || {};
        return this._callbacks[event] || [];
    };

    /**
     * Check if this emitter has `event` handlers.
     *
     * @param {String} event
     * @return {Boolean}
     * @api public
     */

    Emitter.prototype.hasListeners = function(event) {
        return !!this.listeners(event).length;
    };

    var JS_WS_CLIENT_TYPE = 'js-websocket';
    var JS_WS_CLIENT_VERSION = '0.0.1';

    var Protocol = window.Protocol;
    var protobuf = window.protobuf;
    var decodeIO_protobuf = window.decodeIO_protobuf;
    var decodeIO_encoder = null;
    var decodeIO_decoder = null;
    var Package = Protocol.Package;
    var Message = Protocol.Message;
    var EventEmitter = Emitter;
    var rsa = window.rsa;

    if (typeof(window) != "undefined" && typeof(sys) != 'undefined' && sys.localStorage) {
        window.localStorage = sys.localStorage;
    }

    var RES_OK = 200;
    var RES_FAIL = 500;
    var RES_OLD_CLIENT = 501;

    if (typeof Object.create !== 'function') {
        Object.create = function(o) {
            function F() {}
            F.prototype = o;
            return new F();
        };
    }

    var root = window;
    var pomelo = Object.create(EventEmitter.prototype); // object extend from object
    root.pomelo = pomelo;
    var socket = null;
    var reqId = 0;
    var callbacks = {};
    var handlers = {};
    //Map from request id to route
    var routeMap = {};
    var dict = {}; // route string to code
    var abbrs = {}; // code to route string
    var serverProtos = {};
    var clientProtos = {};
    var protoVersion = 0;

    var heartbeatInterval = 0;
    var heartbeatTimeout = 0;
    var nextHeartbeatTimeout = 0;
    var gapThreshold = 100; // heartbeat gap threashold
    var heartbeatId = null;
    var heartbeatTimeoutId = null;
    var handshakeCallback = null;

    var decode = null;
    var encode = null;

    var reconnect = false;
    var reconncetTimer = null;
    var reconnectUrl = null;
    var reconnectAttempts = 0;
    var reconnectionDelay = 5000;
    var DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;

    var useCrypto;
    var protoRoot = {};
    var protoPackage;

    var handshakeBuffer = {
        'sys': {
            type: JS_WS_CLIENT_TYPE,
            version: JS_WS_CLIENT_VERSION,
            rsa: {}
        },
        'user': {}
    };

    var initCallback = null;

    pomelo.init = function(params, cb) {
        initCallback = cb;
        var host = params.host;
        var port = params.port;

        encode = params.encode || defaultEncode;
        decode = params.decode || defaultDecode;
        protoPackage = params.protoPackage || "";

        var url = 'ws://' + host;
        if (port) {
            url += ':' + port;
        }

        handshakeBuffer.user = params.user;
        if (params.encrypt) {
            useCrypto = true;
            rsa.generate(1024, "10001");
            var data = {
                rsa_n: rsa.n.toString(16),
                rsa_e: rsa.e
            }
            handshakeBuffer.sys.rsa = data;
        }
        handshakeCallback = params.handshakeCallback;
        connect(params, url, cb);
    };

    var defaultDecode = pomelo.decode = function(data) {
        //probuff decode
        var msg = Message.decode(data);

        if (msg.id > 0) {
            msg.route = routeMap[msg.id];
            delete routeMap[msg.id];
            if (!msg.route) {
                return;
            }
        }

        msg.body = deCompose(msg);
        return msg;
    };

    var defaultEncode = pomelo.encode = function(reqId, route, msg) {
        var type = reqId ? Message.TYPE_REQUEST : Message.TYPE_NOTIFY;

        //compress message by protobuf
        var proto = clientProtos[route]
        if (protobuf && proto) {
            var awepath = protoPackage ? protoPackage + "." + proto : proto;
            var awemsg = protoRoot.lookupType(awepath);
            var errMsg = awemsg.verify(msg);
            if (errMsg)
                throw Error(errMsg);
            var message = awemsg.create(msg);
            msg = awemsg.encode(message).finish();
        } else if (decodeIO_encoder && decodeIO_encoder.lookup(route)) {
            var Builder = decodeIO_encoder.build(route);
            msg = new Builder(msg).encodeNB();
        } else {
            msg = Protocol.strencode(JSON.stringify(msg));
        }

        var compressRoute = 0;
        if (dict && dict[route]) {
            route = dict[route];
            compressRoute = 1;
        }

        return Message.encode(reqId, type, compressRoute, route, msg);
    };

    var connect = function(params, url, cb) {
        console.log('connect to ' + url);

        var params = params || {};
        var maxReconnectAttempts = params.maxReconnectAttempts || DEFAULT_MAX_RECONNECT_ATTEMPTS;
        reconnectUrl = url;
        //Add protobuf version
        if (window.localStorage && window.localStorage.getItem('protos') && protoVersion === 0) {
            var protos = JSON.parse(window.localStorage.getItem('protos'));

            protoVersion = protos.version || 0;
            serverProtos = protos.server || {};
            clientProtos = protos.client || {};

            console.log("存在不", protobuf);
            // if (!!protobuf) {
            //   protobuf.init({ encoderProtos: clientProtos, decoderProtos: serverProtos });
            // }
            if (!!protobuf && params.protoPath) {
                protobuf.load(params.protoPath, function(err, root) {
                    if (err)
                        throw err;
                    protoRoot = root;
                    pomelo.connectNext(params, url, cb);
                })
            }
            if (!!decodeIO_protobuf) {
                decodeIO_encoder = decodeIO_protobuf.loadJson(clientProtos);
                decodeIO_decoder = decodeIO_protobuf.loadJson(serverProtos);
                pomelo.connectNext(params, url, cb);
            }
        }
    };

    pomelo.connectNext = function(params, url, cb) {
        //Set protoversion
        handshakeBuffer.sys.protoVersion = protoVersion;

        var onopen = function(event) {
            if (!!reconnect) {
                pomelo.emit('reconnect');
            }
            reset();
            var obj = Package.encode(Package.TYPE_HANDSHAKE, Protocol.strencode(JSON.stringify(handshakeBuffer)));
            send(obj);
        };
        var onmessage = function(event) {
            processPackage(Package.decode(event.data), cb);
            // new package arrived, update the heartbeat timeout
            if (heartbeatTimeout) {
                nextHeartbeatTimeout = Date.now() + heartbeatTimeout;
            }
        };
        var onerror = function(event) {
            pomelo.emit('io-error', event);
            console.error('socket error: ', event);
        };
        var onclose = function(event) {
            pomelo.emit('close', event);
            pomelo.emit('disconnect', event);
            console.error('socket close: ', event);
            if (!!params.reconnect && reconnectAttempts < maxReconnectAttempts) {
                reconnect = true;
                reconnectAttempts++;
                reconncetTimer = setTimeout(function() {
                    connect(params, reconnectUrl, cb);
                }, reconnectionDelay);
                reconnectionDelay *= 2;
            }
        };
        socket = new WebSocket(url);
        socket.binaryType = 'arraybuffer';
        socket.onopen = onopen;
        socket.onmessage = onmessage;
        socket.onerror = onerror;
        socket.onclose = onclose;
    }

    pomelo.disconnect = function() {
        if (socket) {
            if (socket.disconnect) socket.disconnect();
            if (socket.close) socket.close();
            console.log('disconnect');
            socket = null;
        }

        if (heartbeatId) {
            clearTimeout(heartbeatId);
            heartbeatId = null;
        }
        if (heartbeatTimeoutId) {
            clearTimeout(heartbeatTimeoutId);
            heartbeatTimeoutId = null;
        }
    };

    var reset = function() {
        reconnect = false;
        reconnectionDelay = 1000 * 5;
        reconnectAttempts = 0;
        clearTimeout(reconncetTimer);
    };

    pomelo.request = function(route, msg, cb) {
        if (arguments.length === 2 && typeof msg === 'function') {
            cb = msg;
            msg = {};
        } else {
            msg = msg || {};
        }
        route = route || msg.route;
        if (!route) {
            return;
        }

        reqId++;
        sendMessage(reqId, route, msg);

        callbacks[reqId] = cb;
        routeMap[reqId] = route;
    };

    pomelo.notify = function(route, msg) {
        msg = msg || {};
        sendMessage(0, route, msg);
    };

    var sendMessage = function(reqId, route, msg) {
        if (useCrypto) {
            msg = JSON.stringify(msg);
            var sig = rsa.signString(msg, "sha256");
            msg = JSON.parse(msg);
            msg['__crypto__'] = sig;
        }

        if (encode) {
            msg = encode(reqId, route, msg);
        }

        var packet = Package.encode(Package.TYPE_DATA, msg);
        send(packet);
    };

    var send = function(packet) {
        if (socket)
            socket.send(packet.buffer);
    };

    var handler = {};

    var heartbeat = function(data) {
        if (!heartbeatInterval) {
            // no heartbeat
            return;
        }

        var obj = Package.encode(Package.TYPE_HEARTBEAT);
        if (heartbeatTimeoutId) {
            clearTimeout(heartbeatTimeoutId);
            heartbeatTimeoutId = null;
        }

        if (heartbeatId) {
            // already in a heartbeat interval
            return;
        }
        heartbeatId = setTimeout(function() {
            heartbeatId = null;
            send(obj);

            nextHeartbeatTimeout = Date.now() + heartbeatTimeout;
            heartbeatTimeoutId = setTimeout(heartbeatTimeoutCb, heartbeatTimeout);
        }, heartbeatInterval);
    };

    var heartbeatTimeoutCb = function() {
        var gap = nextHeartbeatTimeout - Date.now();
        if (gap > gapThreshold) {
            heartbeatTimeoutId = setTimeout(heartbeatTimeoutCb, gap);
        } else {
            console.error('server heartbeat timeout');
            pomelo.emit('heartbeat timeout');
            pomelo.disconnect();
        }
    };

    var handshake = function(data) {
        data = JSON.parse(Protocol.strdecode(data));
        if (data.code === RES_OLD_CLIENT) {
            pomelo.emit('error', 'client version not fullfill');
            return;
        }

        if (data.code !== RES_OK) {
            pomelo.emit('error', 'handshake fail');
            return;
        }

        handshakeInit(data);

        var obj = Package.encode(Package.TYPE_HANDSHAKE_ACK);
        send(obj);
        if (initCallback) {
            initCallback(socket);
        }
    };

    var onData = function(data) {
        var msg = data;
        if (decode) {
            msg = decode(msg);
        }
        processMessage(pomelo, msg);
    };

    var onKick = function(data) {
        data = JSON.parse(Protocol.strdecode(data));
        pomelo.emit('onKick', data);
    };

    handlers[Package.TYPE_HANDSHAKE] = handshake;
    handlers[Package.TYPE_HEARTBEAT] = heartbeat;
    handlers[Package.TYPE_DATA] = onData;
    handlers[Package.TYPE_KICK] = onKick;

    var processPackage = function(msgs) {
        if (Array.isArray(msgs)) {
            for (var i = 0; i < msgs.length; i++) {
                var msg = msgs[i];
                handlers[msg.type](msg.body);
            }
        } else {
            handlers[msgs.type](msgs.body);
        }
    };

    var processMessage = function(pomelo, msg) {
        if (!msg.id) {
            // server push message
            pomelo.emit(msg.route, msg.body);
            return;
        }

        //if have a id then find the callback function with the request
        var cb = callbacks[msg.id];

        delete callbacks[msg.id];
        if (typeof cb !== 'function') {
            return;
        }

        cb(msg.body);
        return;
    };

    var processMessageBatch = function(pomelo, msgs) {
        for (var i = 0, l = msgs.length; i < l; i++) {
            processMessage(pomelo, msgs[i]);
        }
    };

    var deCompose = function(msg) {
        var route = msg.route;

        //Decompose route from dict
        if (msg.compressRoute) {
            if (!abbrs[route]) {
                return {};
            }

            route = msg.route = abbrs[route];
        }
        var proto = serverProtos[route]
        if (protobuf && proto) {
            // Obtain a message type
            var awepath = protoPackage ? protoPackage + "." + proto : proto;
            var awemsg = protoRoot.lookupType(awepath);
            return awemsg.decode(msg.body);
            // return protobuf.decodeStr(route, msg.body);
        } else if (decodeIO_decoder && decodeIO_decoder.lookup(route)) {
            return decodeIO_decoder.build(route).decode(msg.body);
        } else {
            return JSON.parse(Protocol.strdecode(msg.body));
        }

        return msg;
    };

    var handshakeInit = function(data) {
        console.log("handshakeInit:", data)
        if (data.sys && data.sys.heartbeat) {
            heartbeatInterval = data.sys.heartbeat * 1000; // heartbeat interval
            heartbeatTimeout = heartbeatInterval * 2; // max heartbeat timeout
        } else {
            heartbeatInterval = 0;
            heartbeatTimeout = 0;
        }

        initData(data);

        if (typeof handshakeCallback === 'function') {
            handshakeCallback(data.user);
        }
    };

    //Initilize data used in pomelo client
    var initData = function(data) {
        if (!data || !data.sys) {
            return;
        }
        dict = data.sys.dict;
        var protos = data.sys.protos;

        //Init compress dict
        if (dict) {
            dict = dict;
            abbrs = {};

            for (var route in dict) {
                abbrs[dict[route]] = route;
            }
        }
        console.log("initData:", protos)
            //Init protobuf protos
        if (protos) {
            protoVersion = protos.version || 0;
            serverProtos = protos.server || {};
            clientProtos = protos.client || {};

            //Save protobuf protos to localStorage
            window.localStorage.setItem('protos', JSON.stringify(protos));

            // if (!!protobuf) {
            //   protobuf.init({ encoderProtos: protos.client, decoderProtos: protos.server });
            // }
            if (!!decodeIO_protobuf) {
                decodeIO_encoder = decodeIO_protobuf.loadJson(clientProtos);
                decodeIO_decoder = decodeIO_protobuf.loadJson(serverProtos);
            }
        }
    };

    if (typeof(window) != "undefined") {
        window.pomelo = pomelo;
    }
    (typeof(window) == "undefined" ? module.exports : (this.Protocol = {}), typeof(window) == "undefined" ? Buffer : Uint8Array, this);
})();